// TODO: CODE REVIEW & CLEANUP!
//
// UiUtilities.cs
//
// Implements utility types that are helpful to use in Silverlight-based applications.  These
// types are not specific to the HomeUX control framework or HomeUX client application framework.
//

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Xml;
using HomeUX.Utilities;

namespace HomeUX.UiUtilities
{

public static class UiUtil
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // ShowDebugMessage
    //

    static Popup _debugMessagePopup;

    public static void ShowDebugMessage(string format, params object[] args)
    {
        if (_debugMessagePopup != null)
        {
            _debugMessagePopup.IsOpen = false;
            _debugMessagePopup = null;
        }

        if (format == null)
            return;

        string message = Util.Format(format, args);
        var textBlock = new TextBlock()
        {
            Text = message,
            Foreground = new SolidColorBrush(Colors.Yellow),
            FontSize = 10
        };

        var border = new Border()
        {
            Padding = new Thickness(4, 0, 4, 0),
            Background = new SolidColorBrush(Colors.Black),
            Child = textBlock
        };

        _debugMessagePopup = new Popup()
        {
            Child = border,
            IsOpen = true
        };

        border.MouseLeftButtonDown += delegate(object sender, MouseButtonEventArgs e)
        {
            _debugMessagePopup.IsOpen = false;
            _debugMessagePopup = null;
        };
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // EnableTextBlock
    //

    /// <summary>
    /// A cached copy of the "enabled" foreground color used by <r>EnableTextBlock</r>, or
    /// <n>null</n> if it wasn't cached yet.
    /// </summary>
    static Brush _enabledTextForeground;

    /// <summary>
    /// A cached copy of the "disabled" foreground color used by <r>EnableTextBlock</r>, or
    /// <n>null</n> if it wasn't cached yet.
    /// </summary>
    static Brush _disabledTextForeground;

    /// <summary>
    /// Sets the foreground color of a given <n>TextBlock</n> so that the text appears to be
    /// enabled or disabled.
    /// </summary>
    ///
    /// <param name="textBlock">The <n>TextBlock</n> to adjust the display of.</param>
    ///
    /// <param name="enable"><n>true</n>to make the <n>TextBlock</n> have an enabled appearance,
    ///     <n>false</n> to make it have a disabled appearance.</param>
    ///
    /// <remarks>
    /// This method looks for application resources named "EnabledTextForeground" and
    /// "DisabledTextForeground", respectively, which should be <n>Brush</n> objects -- these
    /// will be used as the foreground text color if <pr>enable</pr> is <n>true</n> or
    /// <n>false</n>, respectively.  If these resources are not found, default brushes are used.
    /// </remarks>
    ///
    public static void EnableTextBlock(TextBlock textBlock, bool enable)
    {
        // initialize <_enabledTextForeground> if it hasn't been initialized yet
        if (_enabledTextForeground == null)
        {
            if ((_enabledTextForeground =
                    (Brush)Application.Current.Resources["EnabledTextForeground"]) == null)
                _enabledTextForeground = new SolidColorBrush(Colors.Black);
        }

        // initialize <_disabledTextForeground> if it hasn't been initialized yet
        if (_disabledTextForeground == null)
        {
            const int c = 155; // default R, G, and B component for grayed text
            if ((_disabledTextForeground =
                    (Brush)Application.Current.Resources["DisabledTextForeground"]) == null)
                _disabledTextForeground = new SolidColorBrush(Color.FromArgb(255, c, c, c));
        }
        textBlock.Foreground = enable ? _enabledTextForeground : _disabledTextForeground;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Resource Retrieval
    //

    public static void GetRequiredResource(FrameworkElement element, string resourceName, out Thickness result)
    {
        string stringValue;
        GetRequiredResource(element, resourceName, out stringValue);

        try
        {
            string[] parts = stringValue.Split(',');
            if (parts.Length != 4)
                throw new ArgumentException();
            double left = XmlConvert.ToDouble(parts[0]);
            double top = XmlConvert.ToDouble(parts[1]);
            double right = XmlConvert.ToDouble(parts[2]);
            double bottom = XmlConvert.ToDouble(parts[3]);
            result = new Thickness(left, top, right, bottom);
        }
        catch (Exception ex)
        {
            if ((ex is FormatException) || (ex is OverflowException) || (ex is ArgumentException))
            {
                throw new InvalidOperationException(String.Format(
                    "A resource with x:Key=\"{0}\" was found, but its value (\"{1}\") cannot be converted to a Thickness",
                    resourceName, stringValue));
            }
            else
                throw;
        }
    }

    public static void GetRequiredResource<T>(FrameworkElement element, string resourceName, out T result)
    {
        object resultAsObject = GetRequiredResource(element, resourceName);
        try
        {
            result = (T)resultAsObject;
        }
        catch (InvalidCastException)
        {
            throw NewRequiredResourceIsWrongTypeException(resourceName, resultAsObject, typeof(T));
        }
    }

    static object GetRequiredResource(FrameworkElement element, string resourceName)
    {
        object result = element.Resources[resourceName];
        if (result == null)
        {
            throw new ArgumentException(String.Format(
                "Cannot find resource with x:Name=\"{0}\" within the provided {1}",
                resourceName, element.GetType()));
        }
        return result;
    }

    static Exception NewRequiredResourceIsWrongTypeException(string resourceName, object resource, Type type)
    {
        return new InvalidOperationException(String.Format(
            "A resource with x:Key=\"{0}\" was found, but is of type {1}; expecting {2}",
            resourceName, resource.GetType(), type));
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Measurement
    //

    static public Rect InflateRect(Rect rect, double left, double top, double right, double bottom)
    {
        return new Rect(rect.X - left, rect.Y - top, rect.Width + left + right, rect.Height + top + bottom);
    }

    static public Rect InflateRect(Rect rect, double amount)
    {
        return InflateRect(rect, amount, amount, amount, amount);
    }

    static public Rect GetElementBoundingRect(FrameworkElement innerElement, FrameworkElement outerElement)
    {
        Point topLeft = GetElementTopLeft(innerElement, outerElement);
        return new Rect(topLeft.X, topLeft.Y, innerElement.ActualWidth, innerElement.ActualHeight);
    }

    static public Point GetElementTopLeft(FrameworkElement innerElement, FrameworkElement outerElement)
    {
        FrameworkElement element = innerElement;
        var topLeft = new Point();
        while ((element != null) && (element != outerElement))
        {
            Rect rect = LayoutInformation.GetLayoutSlot(element);
            topLeft.X += rect.Left + element.Margin.Left;
            topLeft.Y += rect.Top + element.Margin.Top;
            element = element.Parent as FrameworkElement;
        }
        return topLeft;
    }

    // seems dumb that I need to do this, but e.g. LayoutInformation.GetLayoutClip, which should help here,
    // often returns null for no apparent reason...
    public static Rect GetBoundingRect(FrameworkElement element, FrameworkElement relativeTo)
    {
        Point ptElement = GetAbsoluteTopLeft(element);
        Point ptRelativeTo = GetAbsoluteTopLeft(relativeTo);
        return new Rect(ptElement.X - ptRelativeTo.X, ptElement.Y - ptRelativeTo.Y,
            element.ActualWidth, element.ActualHeight);
    }

    /// <summary>
    /// Returns the coordinates of the top-left corner of the bounding rectangle of a given
    /// <n>FrameworkElement</n> relative to the entire Silverlight plug-in client area.
    /// </summary>
    /// <param name="element"></param>
    /// <returns></returns>
    public static Point GetAbsoluteTopLeft(FrameworkElement element)
    {
        Point ptAbsolute = new Point(0, 0);
        while (true)
        {
            Point ptRelative = GetRelativeTopLeft(element);
            ptAbsolute.X += ptRelative.X;
            ptAbsolute.Y += ptRelative.Y;
            element = element.Parent as FrameworkElement;
            if (element == null)
                return ptAbsolute;
        }
    }

    /// <summary>
    /// Returns the coordinates of the top-left corner of the bounding rectangle of a given
    /// <n>FrameworkElement</n> relative to its parent.
    /// </summary>
    /// <param name="element"></param>
    /// <returns></returns>
    public static Point GetRelativeTopLeft(FrameworkElement element)
    {
        Rect layoutSlot = LayoutInformation.GetLayoutSlot(element);

        double x;
        switch (element.HorizontalAlignment)
        {
        case HorizontalAlignment.Left:
        case HorizontalAlignment.Stretch:
            x = layoutSlot.Left + element.Margin.Left;
            break;
        case HorizontalAlignment.Center:
            double width = element.Margin.Left + element.ActualWidth + element.Margin.Right;
            x = (layoutSlot.Width - width) / 2;
            break;
        case HorizontalAlignment.Right:
            x = layoutSlot.Right - element.Margin.Right - element.ActualWidth;
            break;
        default:
            throw new InvalidOperationException("Unexpected HorizontalAlignment value");
        }

        double y;
        switch (element.VerticalAlignment)
        {
        case VerticalAlignment.Top:
        case VerticalAlignment.Stretch:
            y = layoutSlot.Top + element.Margin.Top;
            break;
        case VerticalAlignment.Center:
            double width = element.Margin.Top + element.ActualHeight + element.Margin.Bottom;
            y = (layoutSlot.Height - width) / 2;
            break;
        case VerticalAlignment.Bottom:
            y = layoutSlot.Bottom - element.Margin.Bottom - element.ActualHeight;
            break;
        default:
            throw new InvalidOperationException("Unexpected VerticalAlignment value");
        }

        return new Point(x, y);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Miscellaneous
    //

    public static void DisposeVisualTree(DependencyObject root)
    {
        int childCount = VisualTreeHelper.GetChildrenCount(root);
        for (int childIndex = 0; childIndex < childCount; childIndex++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(root, childIndex);
            DisposeVisualTree(child);
            IDisposable disposable = child as IDisposable;
            if (disposable != null)
                disposable.Dispose();
        }
    }
}

}
